//
// Created by HP on 2022/1/31.
//

#include "tmc_shader.h"

// lib
#include <glm/gtc/type_ptr.hpp>

// std
#include <fstream>
#include <exception>

namespace tmc {
    // ---- Shader ----

    Shader::Shader(Shader::ShaderType type, const std::string &sourceCode) {
        shaderID = glCreateShader(static_cast<GLenum>(type));
        auto sourcePtr = sourceCode.c_str();
        glShaderSource(shaderID, 1, &sourcePtr, nullptr);
        glCompileShader(shaderID);

        // check
        int success;
        char infoLog[513]{0};
        glGetShaderiv(shaderID, GL_COMPILE_STATUS, &success);
        if (!success)
        {
            glGetShaderInfoLog(shaderID, 512, nullptr, infoLog);
            // 清理资源
            glDeleteShader(shaderID);
            shaderID = 0;

            std::string logMsg(infoLog);
            switch (type) {
                case ShaderType::Vert:
                    TMC_THROW_ERROR("Failed to compile vertex shader.\nLog message: " + logMsg);
                case ShaderType::Frag:
                    TMC_THROW_ERROR("Failed to compile fragment shader.\nLog message: " + logMsg);
            }
        }

    }

    Shader
    Shader::createShaderFromFile(Shader::ShaderType type, const std::string &filepath) {
        std::ifstream shaderFile{filepath, std::ios::ate};
        if (!shaderFile.is_open()) {
            TMC_THROW_ERROR("failed to open file: " + filepath);
        }
        size_t fileSize = static_cast<size_t>(shaderFile.tellg());
        std::string  buffer(fileSize, '\0');

        shaderFile.seekg(0);
        shaderFile.read(buffer.data(), fileSize);
        shaderFile.close();

        return Shader{type, buffer};
    }

    // ---- ShaderProgram ----

    ShaderProgram::ShaderProgram(Shader vert, Shader frag):
    vertShader(std::move(vert)), fragShader(std::move(frag)) {
        programID = glCreateProgram();
    }

    ShaderProgram::~ShaderProgram() {
        glDeleteProgram(programID);
    }

    void ShaderProgram::compileShaderProgram() {
        // check
        if (vertShader.isNULL() || fragShader.isNULL()) {
            TMC_THROW_ERROR("NULL Shader, please provide a not NULL shader.");
        }

        // compile
        glAttachShader(programID, vertShader.getShaderId());
        glAttachShader(programID, fragShader.getShaderId());
        glLinkProgram(programID);

        // check
        int success;
        char infoLog[513]{0};
        glGetProgramiv(programID, GL_LINK_STATUS, &success);
        if (!success) {
            glGetProgramInfoLog(programID, 512, nullptr, infoLog);
            std::string logMsg(infoLog);

            glDeleteProgram(programID);
            programID = 0;

            TMC_THROW_ERROR("Filed to link shader program\nmsg: " + logMsg);
        }
    }

    void ShaderProgram::use() {
        glUseProgram(programID);
    }

    // https://www.khronos.org/registry/OpenGL-Refpages/gl4/html/glGetUniformLocation.xhtml
    void ShaderProgram::handleError(GLint result) {
        TMC_HANDLE_GL_ERROR();
        if (result == -1) {
            TMC_THROW_ERROR(
                "This error may be caused by: \n"
                "name does not correspond to an active uniform variable in program\n"
                "name starts with the reserved prefix `gl_`\n"
                "name is associated with an atomic counter or a named uniform block\n"
            );
        }
    }

    void ShaderProgram::setBool(const std::string &name, const bool value) const {
        GLint locationResult = glGetUniformLocation(programID, name.c_str());
        ShaderProgram::handleError(locationResult);
        glUniform1i(locationResult, (int)value);
        TMC_HANDLE_GL_ERROR();
    }
    void ShaderProgram::setInt(const std::string &name, const int value) const {
        GLint locationResult = glGetUniformLocation(programID, name.c_str());
        ShaderProgram::handleError(locationResult);
        glUniform1i(locationResult, value);
        TMC_HANDLE_GL_ERROR();
    }
    void ShaderProgram::setFloat(const std::string &name, const float value) const {
        GLint locationResult = glGetUniformLocation(programID, name.c_str());
        ShaderProgram::handleError(locationResult);
        glUniform1f(locationResult, value);
        TMC_HANDLE_GL_ERROR();
    }

    void ShaderProgram::setVec3(const std::string &name, const glm::vec3& value) const {
        GLint locationResult = glGetUniformLocation(programID, name.c_str());
        ShaderProgram::handleError(locationResult);
        glUniform3fv(locationResult, 1, glm::value_ptr(value));
        TMC_HANDLE_GL_ERROR();
    }

    void ShaderProgram::setMat4(const std::string &name, const glm::mat4& value) const {
        GLint locationResult = glGetUniformLocation(programID, name.c_str());
        ShaderProgram::handleError(locationResult);
        glUniformMatrix4fv(locationResult, 1, GL_FALSE, glm::value_ptr(value));
        TMC_HANDLE_GL_ERROR();
    }
}